Coverage Report

Created: 2026-03-18 12:57

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\scloud-dns\scloud-dns\src\config.rs
Line
Count
Source
1
//! Configuration types for scloud-dns
2
//!
3
//! This file contains Serde (Deserialize/Serialize) structs that map to the
4
//! JSON configuration you provided. It includes helpers to load the config
5
//! from a file and a light `validate()` method placeholder you can extend.
6
7
use crate::exceptions::SCloudException;
8
use anyhow::{Context, Result};
9
use serde::{Deserialize, Serialize};
10
use std::collections::HashSet;
11
use std::fs;
12
use std::path::Path;
13
14
/// Top-level configuration
15
#[derive(Debug, Clone, Serialize, Deserialize)]
16
pub struct Config {
17
    #[serde(default)]
18
    pub server: ServerConfig,
19
20
    #[serde(default)]
21
    pub workers: WorkersConfig,
22
23
    #[serde(default)]
24
    pub logging: LoggingConfig,
25
26
    #[serde(default)]
27
    pub metrics: MetricsConfig,
28
29
    #[serde(default)]
30
    pub admin: AdminConfig,
31
32
    #[serde(default)]
33
    pub acl: Vec<AclEntry>,
34
35
    #[serde(default)]
36
    pub listener: Vec<ListenerConfig>,
37
38
    #[serde(default)]
39
    pub doh: DohConfig,
40
41
    #[serde(default)]
42
    pub forwarder: Vec<ForwarderConfig>,
43
44
    #[serde(default)]
45
    pub root_hints: RootHintsConfig,
46
47
    #[serde(default)]
48
    pub cache: CacheConfig,
49
50
    #[serde(default)]
51
    pub recursion: RecursionConfig,
52
53
    #[serde(default)]
54
    pub ratelimit: RateLimitConfig,
55
56
    #[serde(default)]
57
    pub zone: Vec<ZoneConfig>,
58
59
    #[serde(default)]
60
    pub tsig_key: Vec<TsigKey>,
61
62
    #[serde(default)]
63
    pub axfr: AxfrConfig,
64
65
    #[serde(default)]
66
    pub dnssec: DnssecConfig,
67
68
    #[serde(default)]
69
    pub policy: PolicyConfig,
70
71
    #[serde(default)]
72
    pub amplification_mitigation: AmplificationMitigationConfig,
73
74
    #[serde(default)]
75
    pub tuning: TuningConfig,
76
77
    #[serde(default)]
78
    pub view: Vec<ViewConfig>,
79
80
    #[serde(default)]
81
    pub monitoring: MonitoringConfig,
82
83
    #[serde(default)]
84
    pub dynupdate: Vec<DynUpdateConfig>,
85
86
    #[serde(default)]
87
    pub limits: LimitsConfig,
88
}
89
90
impl Config {
91
    /// Load config from a JSON file path
92
9
    pub fn from_file(path: &Path) -> Result<Self, SCloudException> {
93
9
        let s = fs::read_to_string(path)
94
9
            .with_context(|| 
format!0
("reading config file {}",
path0
.
display0
()))
95
9
            .map_err(|_| SCloudException::SCLOUD_CONFIG_FILE_NOT_FOUND)
?0
;
96
9
        let cfg: Config = serde_json::from_str(&s)
97
9
            .context("parsing JSON config")
98
9
            .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_JSON)
?0
;
99
9
        cfg.validate()
?0
;
100
9
        Ok(cfg)
101
9
    }
102
103
    /// Validation hook
104
10
    pub fn validate(&self) -> Result<(), SCloudException> {
105
24
        let 
acl_names10
:
HashSet<&str>10
=
self.acl.iter()10
.
map10
(|a| a.name.as_str()).
collect10
();
106
16
        let 
tsig_names10
:
HashSet<&str>10
=
self.tsig_key.iter()10
.
map10
(|t| t.name.as_str()).
collect10
();
107
10
        let _forwarder_names: HashSet<&str> =
108
24
            
self.forwarder.iter()10
.
map10
(|f| f.name.as_str()).
collect10
();
109
110
72
        let 
is_acl_ref_valid10
= |s: &str| -> bool {
111
72
            if s.trim().is_empty() {
112
0
                return false;
113
72
            }
114
72
            acl_names.contains(s) || 
s16
.
contains16
('/')
115
72
        };
116
117
10
        if self.server.bind_port == 0 {
118
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_SERVER_PORT);
119
10
        }
120
10
        if self.server.max_udp_payload == 0 || self.server.max_udp_payload > 65535 {
121
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_MAX_UDP_PAYLOAD);
122
10
        }
123
10
        if self.tuning.max_label_length == 0 || self.tuning.max_label_length > 63 {
124
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS);
125
10
        }
126
10
        if self.tuning.max_domain_length == 0 || self.tuning.max_domain_length > 253 {
127
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS);
128
10
        }
129
10
        if self.limits.max_udp_packet_size == 0 || self.limits.max_udp_packet_size > 65535 {
130
0
            return Err(SCloudException::SCLOUD_CONFIG_INVALID_DNS_LIMITS);
131
10
        }
132
133
10
        let mut listener_names = HashSet::new();
134
24
        for l in 
&self.listener10
{
135
24
            if l.name.trim().is_empty() {
136
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER);
137
24
            }
138
24
            if !listener_names.insert(l.name.as_str()) {
139
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_LISTENER_NAME);
140
24
            }
141
24
            if l.port == 0 {
142
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PORT);
143
24
            }
144
24
            if l.protocols.is_empty() {
145
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_LISTENER_PROTOCOLS);
146
24
            }
147
24
            if !l.acl.trim().is_empty() && !is_acl_ref_valid(&l.acl) {
148
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
149
24
            }
150
151
24
            if l.enable_tls.unwrap_or(false) {
152
8
                if l.tls_cert_path.as_deref().unwrap_or("").trim().is_empty() {
153
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT);
154
8
                }
155
8
                if l.tls_key_path.as_deref().unwrap_or("").trim().is_empty() {
156
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY);
157
8
                }
158
8
                if !l.protocols.iter().any(|p| matches!(p, Protocol::TCP)) {
159
0
                    return Err(SCloudException::SCLOUD_CONFIG_TLS_REQUIRES_TCP);
160
8
                }
161
16
            }
162
        }
163
164
10
        if self.doh.enabled {
165
8
            if self
166
8
                .doh
167
8
                .tls_cert_path
168
8
                .as_deref()
169
8
                .unwrap_or("")
170
8
                .trim()
171
8
                .is_empty()
172
            {
173
0
                return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_CERT);
174
8
            }
175
8
            if self
176
8
                .doh
177
8
                .tls_key_path
178
8
                .as_deref()
179
8
                .unwrap_or("")
180
8
                .trim()
181
8
                .is_empty()
182
            {
183
0
                return Err(SCloudException::SCLOUD_CONFIG_TLS_MISSING_KEY);
184
8
            }
185
8
            if self.doh.paths.is_empty() {
186
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_DOH);
187
8
            }
188
2
        }
189
190
10
        if self.recursion.enabled {
191
8
            if self.recursion.allowed_acl.trim().is_empty() {
192
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
193
8
            }
194
8
            if !is_acl_ref_valid(&self.recursion.allowed_acl) {
195
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
196
8
            }
197
2
        }
198
199
10
        let mut fwd_names = HashSet::new();
200
24
        for f in 
&self.forwarder10
{
201
24
            if f.name.trim().is_empty() {
202
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER);
203
24
            }
204
24
            if !fwd_names.insert(f.name.as_str()) {
205
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_FORWARDER_NAME);
206
24
            }
207
24
            if f.addresses.is_empty() {
208
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_FORWARDER);
209
24
            }
210
40
            for a in 
&f.addresses24
{
211
40
                if a.parse::<std::net::SocketAddr>().is_err() {
212
0
                    return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR);
213
40
                }
214
            }
215
        }
216
217
10
        let mut zone_names = HashSet::new();
218
32
        for z in 
&self.zone10
{
219
32
            if z.name.trim().is_empty() {
220
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_ZONE);
221
32
            }
222
32
            if !zone_names.insert(z.name.as_str()) {
223
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_ZONE_NAME);
224
32
            }
225
226
32
            match z.kind {
227
                ZoneType::Master => {
228
16
                    let inline = z.inline.unwrap_or(false);
229
16
                    if inline {
230
8
                        if z.records.is_empty() {
231
0
                            return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE);
232
8
                        }
233
8
                        let has_soa = z
234
8
                            .records
235
8
                            .iter()
236
8
                            .any(|r| r.r#type.eq_ignore_ascii_case("SOA"));
237
8
                        if !has_soa {
238
0
                            return Err(SCloudException::SCLOUD_CONFIG_INVALID_INLINE_ZONE);
239
8
                        }
240
                    } else {
241
8
                        if z.file.as_deref().unwrap_or("").trim().is_empty() {
242
0
                            return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE);
243
8
                        }
244
                    }
245
246
16
                    if let Some(
acl8
) = z.notify_acl.as_deref() {
247
8
                        if !acl.trim().is_empty() && !is_acl_ref_valid(acl) {
248
0
                            return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
249
8
                        }
250
8
                    }
251
16
                    if let Some(
acl8
) = z.allow_transfer_acl.as_deref() {
252
8
                        if !acl.trim().is_empty() && !is_acl_ref_valid(acl) {
253
0
                            return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
254
8
                        }
255
8
                    }
256
257
16
                    if let Some(
k8
) = z.axfr_tsig_key.as_deref() {
258
8
                        if !k.trim().is_empty() && !tsig_names.contains(k) {
259
0
                            return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY);
260
8
                        }
261
8
                    }
262
                }
263
                ZoneType::Slave => {
264
8
                    if z.masters.is_empty() {
265
0
                        return Err(SCloudException::SCLOUD_CONFIG_SLAVE_MISSING_MASTERS);
266
8
                    }
267
8
                    for m in &z.masters {
268
8
                        if m.parse::<std::net::SocketAddr>().is_err() {
269
0
                            return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR);
270
8
                        }
271
                    }
272
8
                    if z.file.as_deref().unwrap_or("").trim().is_empty() {
273
0
                        return Err(SCloudException::SCLOUD_CONFIG_ZONE_MISSING_FILE);
274
8
                    }
275
                }
276
                ZoneType::Forward => {
277
8
                    if z.forwarders.is_empty() {
278
0
                        return Err(SCloudException::SCLOUD_CONFIG_FORWARD_ZONE_MISSING_FORWARDERS);
279
8
                    }
280
8
                    for f in &z.forwarders {
281
8
                        if f.parse::<std::net::SocketAddr>().is_err() {
282
0
                            return Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR);
283
8
                        }
284
                    }
285
                }
286
0
                ZoneType::Stub => {
287
0
                    // TODO: not defined JSON yet, strict checks later when I will implement it.
288
0
                }
289
            }
290
291
48
            for r in 
&z.records32
{
292
48
                if r.r#type.eq_ignore_ascii_case("MX") {
293
8
                    if r.priority.is_none() {
294
0
                        return Err(SCloudException::SCLOUD_CONFIG_MX_MISSING_PRIORITY);
295
8
                    }
296
40
                } else if r.priority.is_some() {
297
0
                    return Err(SCloudException::SCLOUD_CONFIG_PRIORITY_ON_NON_MX);
298
40
                }
299
            }
300
        }
301
302
10
        let mut view_names = HashSet::new();
303
16
        for v in 
&self.view10
{
304
16
            if v.name.trim().is_empty() {
305
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW);
306
16
            }
307
16
            if !view_names.insert(v.name.as_str()) {
308
0
                return Err(SCloudException::SCLOUD_CONFIG_DUPLICATE_VIEW_NAME);
309
16
            }
310
16
            if v.acl.trim().is_empty() || !is_acl_ref_valid(&v.acl) {
311
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
312
16
            }
313
16
            for vz in &v.zones {
314
16
                if vz.name.trim().is_empty() || vz.file.trim().is_empty() {
315
0
                    return Err(SCloudException::SCLOUD_CONFIG_INVALID_VIEW);
316
16
                }
317
            }
318
        }
319
320
10
        for 
d8
in &self.dynupdate {
321
8
            if d.zone.trim().is_empty() {
322
0
                return Err(SCloudException::SCLOUD_CONFIG_INVALID_DYNUPDATE);
323
8
            }
324
8
            if d.acl.trim().is_empty() || !is_acl_ref_valid(&d.acl) {
325
0
                return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_ACL_REFERENCE);
326
8
            }
327
8
            if let Some(k) = d.tsig_key.as_deref() {
328
8
                if !k.trim().is_empty() && !tsig_names.contains(k) {
329
0
                    return Err(SCloudException::SCLOUD_CONFIG_UNKNOWN_TSIG_KEY);
330
8
                }
331
0
            }
332
333
8
            if !zone_names.contains(d.zone.as_str()) {
334
0
                return Err(SCloudException::SCLOUD_CONFIG_DYNUPDATE_UNKNOWN_ZONE);
335
8
            }
336
        }
337
338
10
        Ok(())
339
10
    }
340
341
    /// Get the address of a specific forwarder by index value
342
    #[allow(unused)]
343
5
    pub(crate) fn try_get_forwarder_addr_by_index(
344
5
        &self,
345
5
        forwarder_index: usize,
346
5
        address_index: usize,
347
5
    ) -> Result<std::net::SocketAddr, SCloudException> {
348
5
        let addr = self
349
5
            .forwarder
350
5
            .get(forwarder_index)
351
5
            .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)
?0
352
            .addresses
353
5
            .get(address_index)
354
5
            .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_ADDRESS)
?0
355
5
            .parse()
356
5
            .map_err(|_| SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR)
?0
;
357
358
5
        Ok(addr)
359
5
    }
360
361
    // TODO: add a loop to test the next address for each retry
362
5
    pub(crate) fn try_get_forwarder_addr_by_name(
363
5
        &self,
364
5
        forwarder_name: &str,
365
5
    ) -> Result<std::net::SocketAddr, SCloudException> {
366
5
        let forwarder = self
367
5
            .forwarder
368
5
            .iter()
369
12
            .
find5
(|f| f.name == forwarder_name)
370
5
            .ok_or(SCloudException::SCLOUD_CONFIG_MISSING_FORWARDER)
?0
;
371
372
5
        for addr_str in &forwarder.addresses {
373
5
            if let Ok(addr) = addr_str.parse::<std::net::SocketAddr>() {
374
5
                return Ok(addr);
375
0
            }
376
        }
377
378
0
        Err(SCloudException::SCLOUD_CONFIG_IMPOSSIBLE_TO_PARSE_ADDR)
379
5
    }
380
}
381
382
impl Default for Config {
383
5
    fn default() -> Self {
384
5
        Self {
385
5
            server: ServerConfig::default(),
386
5
            workers: WorkersConfig::default(),
387
5
            logging: LoggingConfig::default(),
388
5
            metrics: MetricsConfig::default(),
389
5
            admin: AdminConfig::default(),
390
5
            acl: Vec::new(),
391
5
            listener: Vec::new(),
392
5
            doh: DohConfig::default(),
393
5
            forwarder: Vec::new(),
394
5
            root_hints: RootHintsConfig::default(),
395
5
            cache: CacheConfig::default(),
396
5
            recursion: RecursionConfig::default(),
397
5
            ratelimit: RateLimitConfig::default(),
398
5
            zone: Vec::new(),
399
5
            tsig_key: Vec::new(),
400
5
            axfr: AxfrConfig::default(),
401
5
            dnssec: DnssecConfig::default(),
402
5
            policy: PolicyConfig::default(),
403
5
            amplification_mitigation: AmplificationMitigationConfig::default(),
404
5
            tuning: TuningConfig::default(),
405
5
            view: Vec::new(),
406
5
            monitoring: MonitoringConfig::default(),
407
5
            dynupdate: Vec::new(),
408
5
            limits: LimitsConfig::default(),
409
5
        }
410
5
    }
411
}
412
413
#[derive(Debug, Clone, Serialize, Deserialize)]
414
pub struct ServerConfig {
415
    pub name: String,
416
    pub version: String,
417
    pub environment: String,
418
    pub max_concurrent_requests: usize,
419
    pub graceful_shutdown_timeout_secs: u64,
420
421
    pub default_ttl: u32,
422
    pub max_udp_payload: usize,
423
    pub enable_edns: bool,
424
    pub enable_tcp: bool,
425
    pub enable_dnssec: bool,
426
427
    pub bind_port: u16,
428
}
429
430
impl Default for ServerConfig {
431
6
    fn default() -> Self {
432
6
        ServerConfig {
433
6
            name: "scloud-dns".to_string(),
434
6
            version: "none".to_string(),
435
6
            environment: "production".to_string(),
436
6
            max_concurrent_requests: 5000,
437
6
            graceful_shutdown_timeout_secs: 15,
438
6
            default_ttl: 3600,
439
6
            max_udp_payload: 4096,
440
6
            enable_edns: true,
441
6
            enable_tcp: true,
442
6
            enable_dnssec: false,
443
6
            bind_port: 53,
444
6
        }
445
6
    }
446
}
447
448
#[derive(Debug, Clone, Serialize, Deserialize)]
449
pub struct WorkersConfig {
450
    pub listener: u16,
451
    pub decoder: u16,
452
    pub query_dispatcher: u16,
453
    pub cache_lookup: u16,
454
    pub zone_manager: u16,
455
    pub resolver: u16,
456
    pub cache_writer: u16,
457
    pub encoder: u16,
458
    pub sender: u16,
459
    pub cache_janitor: u16,
460
    pub metrics: u16,
461
    pub tcp_acceptor: u16,
462
}
463
464
impl Default for WorkersConfig {
465
5
    fn default() -> Self {
466
5
        WorkersConfig {
467
5
            listener: 5,
468
5
            decoder: 5,
469
5
            query_dispatcher: 3,
470
5
            cache_lookup: 3,
471
5
            zone_manager: 1,
472
5
            resolver: 5,
473
5
            cache_writer: 1,
474
5
            encoder: 5,
475
5
            sender: 5,
476
5
            cache_janitor: 1,
477
5
            metrics: 2,
478
5
            tcp_acceptor: 1,
479
5
        }
480
5
    }
481
}
482
483
#[derive(Debug, Clone, Serialize, Deserialize)]
484
pub struct LoggingConfig {
485
    pub level: LogLevel,
486
    pub format: LogFormat,
487
    pub file: String,
488
    pub rotate: bool,
489
    pub live_print: bool,
490
    pub max_size_mb: u64,
491
}
492
493
impl Default for LoggingConfig {
494
5
    fn default() -> Self {
495
5
        LoggingConfig {
496
5
            level: LogLevel::INFO,
497
5
            format: LogFormat::TEXT,
498
5
            file: "/var/log/scloud-dns/scloud-dns.log".to_string(),
499
5
            rotate: true,
500
5
            live_print: false,
501
5
            max_size_mb: 200,
502
5
        }
503
5
    }
504
}
505
506
#[allow(non_camel_case_types)]
507
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
508
#[serde(rename_all = "lowercase")]
509
pub enum LogLevel {
510
    TRACE = 0,
511
    DEBUG = 1,
512
    INFO = 2,
513
    WARN = 3,
514
    ERROR = 4,
515
    FATAL = 5,
516
}
517
518
impl LogLevel {
519
0
    pub fn parse(s: &str) -> Self {
520
0
        match s.to_ascii_lowercase().as_str() {
521
0
            "trace" => Self::TRACE,
522
0
            "debug" => Self::DEBUG,
523
0
            "info" => Self::INFO,
524
0
            "warn" | "warning" => Self::WARN,
525
0
            "error" => Self::ERROR,
526
0
            "fatal" => Self::FATAL,
527
0
            _ => Self::WARN,
528
        }
529
0
    }
530
531
25
    pub(crate) fn as_str(self) -> &'static str {
532
25
        match self {
533
1
            Self::TRACE => "trace",
534
19
            Self::DEBUG => "debug",
535
4
            Self::INFO => "info",
536
0
            Self::WARN => "warn",
537
1
            Self::ERROR => "error",
538
0
            Self::FATAL => "fatal",
539
        }
540
25
    }
541
}
542
543
#[allow(non_camel_case_types)]
544
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
545
#[serde(rename_all = "lowercase")]
546
pub enum LogFormat {
547
    JSON,
548
    TEXT,
549
}
550
551
impl LogFormat {
552
0
    pub fn parse(s: &str) -> Self {
553
0
        match s.to_ascii_lowercase().as_str() {
554
0
            "json" => Self::JSON,
555
0
            _ => Self::TEXT,
556
        }
557
0
    }
558
}
559
560
#[derive(Debug, Clone, Serialize, Deserialize)]
561
pub struct MetricsConfig {
562
    pub enabled: bool,
563
    pub prometheus_bind: String,
564
    pub enable_health_endpoint: bool,
565
    pub health_bind: String,
566
}
567
568
impl Default for MetricsConfig {
569
5
    fn default() -> Self {
570
5
        MetricsConfig {
571
5
            enabled: true,
572
5
            prometheus_bind: "0.0.0.0:9153".to_string(),
573
5
            enable_health_endpoint: true,
574
5
            health_bind: "127.0.0.1:8081".to_string(),
575
5
        }
576
5
    }
577
}
578
579
#[derive(Debug, Clone, Serialize, Deserialize)]
580
pub struct AdminConfig {
581
    pub enabled: bool,
582
    pub bind: String,
583
    pub auth_token: String,
584
    pub enable_tls: bool,
585
}
586
587
impl Default for AdminConfig {
588
5
    fn default() -> Self {
589
5
        AdminConfig {
590
5
            enabled: true,
591
5
            bind: "127.0.0.1:8053".to_string(),
592
5
            auth_token: "replace-with-secure-token".to_string(),
593
5
            enable_tls: false,
594
5
        }
595
5
    }
596
}
597
598
#[derive(Debug, Clone, Serialize, Deserialize)]
599
pub struct AclEntry {
600
    pub name: String,
601
    pub networks: Vec<String>, // CIDRs or single IPs; parse later with ipnet or similar
602
}
603
604
#[derive(Debug, Clone, Serialize, Deserialize)]
605
pub struct ListenerConfig {
606
    pub name: String,
607
    pub address: String,
608
    pub port: u16,
609
    #[serde(default)]
610
    pub protocols: Vec<Protocol>,
611
    #[serde(default)]
612
    pub recursion_allowed: bool,
613
    /// ACL name or a raw CIDR/list string
614
    #[serde(default)]
615
    pub acl: String,
616
    #[serde(default)]
617
    pub workers: Option<usize>,
618
    #[serde(default)]
619
    pub enable_tls: Option<bool>,
620
    #[serde(default)]
621
    pub tls_cert_path: Option<String>,
622
    #[serde(default)]
623
    pub tls_key_path: Option<String>,
624
}
625
626
impl Default for ListenerConfig {
627
1
    fn default() -> Self {
628
1
        ListenerConfig {
629
1
            name: String::new(),
630
1
            address: "0.0.0.0".to_string(),
631
1
            port: 53,
632
1
            protocols: vec![Protocol::UDP],
633
1
            recursion_allowed: false,
634
1
            acl: "0.0.0.0/0".to_string(),
635
1
            workers: None,
636
1
            enable_tls: None,
637
1
            tls_cert_path: None,
638
1
            tls_key_path: None,
639
1
        }
640
1
    }
641
}
642
643
#[derive(Debug, Clone, Serialize, Deserialize)]
644
#[serde(rename_all = "lowercase")]
645
pub enum Protocol {
646
    UDP,
647
    TCP,
648
}
649
650
#[derive(Debug, Clone, Serialize, Deserialize)]
651
pub struct DohConfig {
652
    pub enabled: bool,
653
    pub bind: String,
654
    #[serde(default)]
655
    pub tls_cert_path: Option<String>,
656
    #[serde(default)]
657
    pub tls_key_path: Option<String>,
658
    #[serde(default)]
659
    pub paths: Vec<String>,
660
    #[serde(default)]
661
    pub allowed_origins: Vec<String>,
662
}
663
664
impl Default for DohConfig {
665
6
    fn default() -> Self {
666
6
        DohConfig {
667
6
            enabled: false,
668
6
            bind: "0.0.0.0:443".to_string(),
669
6
            tls_cert_path: None,
670
6
            tls_key_path: None,
671
6
            paths: vec!["/dns-query".to_string()],
672
6
            allowed_origins: Vec::new(),
673
6
        }
674
6
    }
675
}
676
677
#[derive(Debug, Clone, Serialize, Deserialize)]
678
pub struct ForwarderConfig {
679
    pub name: String,
680
    pub addresses: Vec<String>,
681
    pub policy: ForwardPolicy,
682
    pub timeout_ms: u64,
683
    pub edns: bool,
684
    pub use_tcp_on_retry: Option<bool>,
685
}
686
687
impl Default for ForwarderConfig {
688
1
    fn default() -> Self {
689
1
        ForwarderConfig {
690
1
            name: String::new(),
691
1
            addresses: Vec::new(),
692
1
            policy: ForwardPolicy::First,
693
1
            timeout_ms: 1500,
694
1
            edns: true,
695
1
            use_tcp_on_retry: Some(true),
696
1
        }
697
1
    }
698
}
699
700
#[derive(Debug, Clone, Serialize, Deserialize)]
701
#[serde(rename_all = "snake_case")]
702
#[derive(PartialEq)]
703
pub enum ForwardPolicy {
704
    RoundRobin,
705
    First,
706
    Random,
707
}
708
709
#[derive(Debug, Clone, Serialize, Deserialize)]
710
pub struct RootHintsConfig {
711
    pub file: String,
712
}
713
714
impl Default for RootHintsConfig {
715
5
    fn default() -> Self {
716
5
        RootHintsConfig {
717
5
            file: "/etc/scloud/root.hints".to_string(),
718
5
        }
719
5
    }
720
}
721
722
#[derive(Debug, Clone, Serialize, Deserialize)]
723
pub struct CacheConfig {
724
    pub enabled: bool,
725
    pub max_entries: usize,
726
    pub max_ttl_seconds: u64,
727
    pub negative_ttl_seconds: u64,
728
    pub eviction_policy: String,
729
}
730
731
impl Default for CacheConfig {
732
6
    fn default() -> Self {
733
6
        CacheConfig {
734
6
            enabled: true,
735
6
            max_entries: 200_000,
736
6
            max_ttl_seconds: 86_400,
737
6
            negative_ttl_seconds: 300,
738
6
            eviction_policy: "lru".to_string(),
739
6
        }
740
6
    }
741
}
742
743
#[derive(Debug, Clone, Serialize, Deserialize)]
744
pub struct RecursionConfig {
745
    pub enabled: bool,
746
    pub allowed_acl: String,
747
    pub max_recursive_queries: usize,
748
    pub recursion_timeout_ms: u64,
749
    pub retry_interval_ms: u64,
750
}
751
752
impl Default for RecursionConfig {
753
6
    fn default() -> Self {
754
6
        RecursionConfig {
755
6
            enabled: false,
756
6
            allowed_acl: "internal".to_string(),
757
6
            max_recursive_queries: 50,
758
6
            recursion_timeout_ms: 5000,
759
6
            retry_interval_ms: 200,
760
6
        }
761
6
    }
762
}
763
764
#[derive(Debug, Clone, Serialize, Deserialize)]
765
pub struct RateLimitConfig {
766
    pub enabled: bool,
767
    pub global_qps: u64,
768
    pub per_ip_qps: u64,
769
    pub per_subnet_qps: u64,
770
    pub rrl: RrlConfig,
771
}
772
773
impl Default for RateLimitConfig {
774
6
    fn default() -> Self {
775
6
        RateLimitConfig {
776
6
            enabled: true,
777
6
            global_qps: 3000,
778
6
            per_ip_qps: 100,
779
6
            per_subnet_qps: 1000,
780
6
            rrl: RrlConfig::default(),
781
6
        }
782
6
    }
783
}
784
785
#[derive(Debug, Clone, Serialize, Deserialize)]
786
pub struct RrlConfig {
787
    pub enabled: bool,
788
    pub window_seconds: u64,
789
    pub slip: u32,
790
    pub qps_threshold: u64,
791
}
792
793
impl Default for RrlConfig {
794
6
    fn default() -> Self {
795
6
        RrlConfig {
796
6
            enabled: true,
797
6
            window_seconds: 5,
798
6
            slip: 2,
799
6
            qps_threshold: 50,
800
6
        }
801
6
    }
802
}
803
804
#[derive(Debug, Clone, Serialize, Deserialize)]
805
pub struct ZoneConfig {
806
    pub name: String,
807
    #[serde(rename = "type")]
808
    pub kind: ZoneType,
809
    #[serde(default)]
810
    pub file: Option<String>,
811
    #[serde(default)]
812
    pub notify: Option<bool>,
813
    #[serde(default)]
814
    pub notify_acl: Option<String>,
815
    #[serde(default)]
816
    pub allow_transfer_acl: Option<String>,
817
    #[serde(default)]
818
    pub allow_update_acl: Option<String>,
819
    #[serde(default)]
820
    pub axfr_tsig_key: Option<String>,
821
822
    // Slave-specific
823
    #[serde(default)]
824
    pub masters: Vec<String>,
825
826
    // Inline zone
827
    #[serde(default)]
828
    pub inline: Option<bool>,
829
    #[serde(default)]
830
    pub records: Vec<ZoneRecord>,
831
832
    // Forward-specific
833
    #[serde(default)]
834
    pub forwarders: Vec<String>,
835
    #[serde(default)]
836
    pub forward_policy: Option<String>,
837
}
838
839
impl Default for ZoneConfig {
840
1
    fn default() -> Self {
841
1
        ZoneConfig {
842
1
            name: String::new(),
843
1
            kind: ZoneType::Master,
844
1
            file: None,
845
1
            notify: Some(false),
846
1
            notify_acl: None,
847
1
            allow_transfer_acl: None,
848
1
            allow_update_acl: None,
849
1
            axfr_tsig_key: None,
850
1
            masters: Vec::new(),
851
1
            inline: Some(false),
852
1
            records: Vec::new(),
853
1
            forwarders: Vec::new(),
854
1
            forward_policy: None,
855
1
        }
856
1
    }
857
}
858
859
#[derive(Debug, Clone, Serialize, Deserialize)]
860
#[serde(rename_all = "lowercase")]
861
#[derive(PartialEq)]
862
pub enum ZoneType {
863
    Master,
864
    Slave,
865
    Forward,
866
    Stub,
867
}
868
869
#[derive(Debug, Clone, Serialize, Deserialize)]
870
pub struct ZoneRecord {
871
    pub name: String,
872
    pub ttl: Option<u32>,
873
    pub class: Option<String>,
874
    #[serde(rename = "type")]
875
    pub r#type: String,
876
    pub rdata: String,
877
    #[serde(default)]
878
    pub priority: Option<u16>,
879
}
880
881
#[derive(Debug, Clone, Serialize, Deserialize)]
882
pub struct TsigKey {
883
    pub name: String,
884
    pub algorithm: String,
885
    pub secret: String, // TODO: base64 encoded - do not keep in plaintext in production
886
}
887
888
#[derive(Debug, Clone, Serialize, Deserialize)]
889
pub struct AxfrConfig {
890
    pub enabled: bool,
891
    pub max_concurrent_transfers: usize,
892
    pub transfer_timeout_secs: u64,
893
}
894
895
impl Default for AxfrConfig {
896
6
    fn default() -> Self {
897
6
        AxfrConfig {
898
6
            enabled: true,
899
6
            max_concurrent_transfers: 4,
900
6
            transfer_timeout_secs: 120,
901
6
        }
902
6
    }
903
}
904
905
#[derive(Debug, Clone, Serialize, Deserialize)]
906
pub struct DnssecConfig {
907
    pub enabled: bool,
908
    pub auto_sign: bool,
909
    pub default_algo: String,
910
    pub kasp_file: Option<String>,
911
}
912
913
impl Default for DnssecConfig {
914
6
    fn default() -> Self {
915
6
        DnssecConfig {
916
6
            enabled: false,
917
6
            auto_sign: false,
918
6
            default_algo: "RSASHA256".to_string(),
919
6
            kasp_file: None,
920
6
        }
921
6
    }
922
}
923
924
#[derive(Debug, Clone, Serialize, Deserialize)]
925
pub struct PolicyConfig {
926
    #[serde(default)]
927
    pub deny_domains: Vec<String>,
928
}
929
930
impl Default for PolicyConfig {
931
5
    fn default() -> Self {
932
5
        PolicyConfig {
933
5
            deny_domains: Vec::new(),
934
5
        }
935
5
    }
936
}
937
938
#[derive(Debug, Clone, Serialize, Deserialize)]
939
pub struct AmplificationMitigationConfig {
940
    pub drop_fragments: bool,
941
    pub max_response_size_udp: usize,
942
}
943
944
impl Default for AmplificationMitigationConfig {
945
5
    fn default() -> Self {
946
5
        AmplificationMitigationConfig {
947
5
            drop_fragments: true,
948
5
            max_response_size_udp: 4096,
949
5
        }
950
5
    }
951
}
952
953
#[derive(Debug, Clone, Serialize, Deserialize)]
954
pub struct TuningConfig {
955
    pub socket_recv_buffer_bytes: usize,
956
    pub socket_send_buffer_bytes: usize,
957
    pub max_label_length: usize,
958
    pub max_domain_length: usize,
959
}
960
961
impl Default for TuningConfig {
962
5
    fn default() -> Self {
963
5
        TuningConfig {
964
5
            socket_recv_buffer_bytes: 262_144,
965
5
            socket_send_buffer_bytes: 262_144,
966
5
            max_label_length: 63,
967
5
            max_domain_length: 253,
968
5
        }
969
5
    }
970
}
971
972
#[derive(Debug, Clone, Serialize, Deserialize)]
973
pub struct ViewConfig {
974
    pub name: String,
975
    pub acl: String,
976
    #[serde(default)]
977
    pub zones: Vec<ViewZone>,
978
}
979
980
#[derive(Debug, Clone, Serialize, Deserialize)]
981
pub struct ViewZone {
982
    pub name: String,
983
    pub file: String,
984
}
985
986
#[derive(Debug, Clone, Serialize, Deserialize)]
987
pub struct MonitoringConfig {
988
    pub enable_query_logging: bool,
989
    pub query_log_path: String,
990
    pub log_query_qps: u64,
991
}
992
993
impl Default for MonitoringConfig {
994
5
    fn default() -> Self {
995
5
        MonitoringConfig {
996
5
            enable_query_logging: false,
997
5
            query_log_path: "/var/log/scloud-dns/queries.log".to_string(),
998
5
            log_query_qps: 1000,
999
5
        }
1000
5
    }
1001
}
1002
1003
#[derive(Debug, Clone, Serialize, Deserialize)]
1004
pub struct DynUpdateConfig {
1005
    pub zone: String,
1006
    pub acl: String,
1007
    pub tsig_key: Option<String>,
1008
    pub allow: bool,
1009
}
1010
1011
#[derive(Debug, Clone, Serialize, Deserialize)]
1012
pub struct LimitsConfig {
1013
    pub max_udp_packet_size: usize,
1014
    pub max_queries_per_minute_per_ip: u64,
1015
    pub max_tcp_sessions_per_ip: usize,
1016
}
1017
1018
impl Default for LimitsConfig {
1019
6
    fn default() -> Self {
1020
6
        LimitsConfig {
1021
6
            max_udp_packet_size: 4096,
1022
6
            max_queries_per_minute_per_ip: 1000,
1023
6
            max_tcp_sessions_per_ip: 8,
1024
6
        }
1025
6
    }
1026
}